package com.tinkerapps.protocols.ssdp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

/**
 * Class encapsulating functionality of a control point 
 */
public class SSDPControlPoint {

	Thread notificationsListenerThread;
	final SSDPSocket ssdpSocket = new SSDPSocket();
	final DeviceNotificationSink notificationSink;
	final Map<String, SSDPDevice> devices = new HashMap<String, SSDPDevice>();
	
	/**
	 * Constructor
	 */
	public SSDPControlPoint(DeviceNotificationSink sink) {
		if (sink == null)
			throw new IllegalArgumentException("argument 'sink' is null");
		notificationSink = sink;
	}

	/**
	 * Open the control point. Control point should be open to perform any operations
	 * @throws IOException 
	 */
	public void open() throws IOException {
		ssdpSocket.open();
	}

	/**
	 * Close the control point. Control point should be closed after it's no longer needed 
	 * @throws IOException 
	 * @throws InterruptedException 
	 */
	public void close() throws IOException, InterruptedException {
		ssdpSocket.close();
		if (notificationsListenerThread != null) {
			notificationsListenerThread.join();
			notificationsListenerThread = null;
		}
	}
	
	/**
	 * Run devices discovery 
	 * @param what Specifies what to discover. Should be one of SSDP.DISCOVER_XXXX constants
	 * @param maxReplyTime Maximum reply time (in seconds) to use. Should be >= 1 
	 * @param sink Notification sink to use. Caller provides the sink to be notified when
	 */
	public void discover(final String what, final int maxReplyTime) {
		if (maxReplyTime < 1)
			throw new IllegalArgumentException("argument 'maxReplyTime' has invalid value of " + Integer.toString(maxReplyTime));
		
		
		try {
			final CountDownLatch receivingThreadStarted = new CountDownLatch(1); 
			final CountDownLatch startListening = new CountDownLatch(1); 
			
			// Make suer thread listening for notifications is running
			if (notificationsListenerThread == null) {
				notificationsListenerThread = new Thread(new Runnable() {
					@Override
					public void run() {
						byte[] buffer = new byte[1024*2];
						
						try {
							receivingThreadStarted.countDown();
							startListening.await();
							
							// Receive while not closed
							while (true)
							{
								DatagramPacket packet = ssdpSocket.receive(buffer);

								if (packet == null)
									break;

								String response = new String(Arrays.copyOf(packet.getData(),packet.getLength()));

								if (false) {
									for (String line : response.split("\r\n")) {
										System.out.println("    " + line);
									}
								}
								
								String deviceId = SSDPResponse.getDeviceId(response);
								
								// Normalize the ID
								if (deviceId != null)
									deviceId = deviceId.toLowerCase();
								
								if (SSDPResponse.isResponse(response)) {
									if (!devices.containsKey(deviceId)) {
										SSDPDevice device = new SSDPDevice(deviceId, SSDPResponse.getLocationURL(response));
										notificationSink.onDeviceAdded(device);
										devices.put(deviceId, device);
									}
								}
								else if (SSDPResponse.isNotify(response)) {
									if (SSDPResponse.isByeByeNotification(response) && devices.containsKey(deviceId)) {
										notificationSink.onDeviceRemoved(devices.get(deviceId));
										devices.remove(deviceId);
									}
									else if (SSDPResponse.isAliveNotification(response)) {
										// TODO: process advertisement expiration (CACHE-CONTROL header)
										if (!devices.containsKey(deviceId)) {
											SSDPDevice device = new SSDPDevice(deviceId, SSDPResponse.getLocationURL(response));
											notificationSink.onDeviceAdded(device);
											devices.put(deviceId, device);
										}
									}
								}
							}
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				});
				notificationsListenerThread.start();
				receivingThreadStarted.await();
				startListening.countDown();
			}
			
			ssdpSocket.send(SSDPRequest.buildSearchRequest(what, maxReplyTime).getBytes());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
